﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Xml.Serialization;
using CoreLab.Oracle;

namespace Common.Utilities.DLR
{
    /// <summary></summary>
    public enum DataPipeSerializationMethod
    {
        /// <summary></summary>
        SaveToDB = 0,
        /// <summary></summary>
        SaveToXmlFile = 1,
        /// <summary></summary>
        SaveToBinaryFile = 2
    }

    /// <summary>
    /// 
    /// </summary>
    public class DataPipeSerializer
    {
        #region fields
        private DataPipeSerializationMethod _SerializationMethod;
        private string _CustomTypeAssemblyPath;
        private string _CustomTypeName;
        private DataProviderType _TgtDbProvider;
        private string _ConnStr;
        private string _FolderPath;
        private string _FileNameFormat;
        #endregion

        #region ctor
        /// <summary>
        /// 
        /// </summary>
        /// <param name="assemblyPath"></param>
        /// <param name="typeName"></param>
        /// <param name="dbProvider"></param>
        /// <param name="connStr"></param>
        public DataPipeSerializer(string assemblyPath, string typeName, DataProviderType dbProvider, string connStr)
        {
            this._SerializationMethod = DataPipeSerializationMethod.SaveToDB;
            this._CustomTypeAssemblyPath = assemblyPath;
            this._CustomTypeName = typeName;
            this._TgtDbProvider = dbProvider;
            this._ConnStr = connStr;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assemblyPath"></param>
        /// <param name="typeName"></param>
        /// <param name="folderPath"></param>
        /// <param name="fileNameFormat"></param>
        public DataPipeSerializer(string assemblyPath, string typeName, string folderPath, string fileNameFormat)
        {
            this._SerializationMethod = DataPipeSerializationMethod.SaveToBinaryFile;
            this._CustomTypeAssemblyPath = assemblyPath;
            this._CustomTypeName = typeName;
            this._FolderPath = folderPath;
            this._FileNameFormat = fileNameFormat;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assemblyPath"></param>
        /// <param name="typeName"></param>
        /// <param name="filePath"></param>
        public DataPipeSerializer(string assemblyPath, string typeName, string filePath)
        {
            this._SerializationMethod = DataPipeSerializationMethod.SaveToXmlFile;
            this._CustomTypeAssemblyPath = assemblyPath;
            this._CustomTypeName = typeName;
            this._FolderPath = Path.GetDirectoryName(filePath);
            this._FileNameFormat = Path.GetFileName(filePath);
        }
        #endregion

        #region serialize
        /// <summary>
        /// 
        /// </summary>
        /// <param name="data"></param>
        /// <param name="logFilePath"></param>
        /// <param name="errFilePath"></param>
        public void Serialize(List<object> data, string logFilePath, string errFilePath)
        {
            switch(this._SerializationMethod)
            {
                case DataPipeSerializationMethod.SaveToDB:
                    this.SaveDataToDB(data, logFilePath, errFilePath);
                    break;
                case DataPipeSerializationMethod.SaveToBinaryFile:

                    break;
                case DataPipeSerializationMethod.SaveToXmlFile:

                    break;
                default:
                    break;
            }
        }

        private void SaveDataToDB(List<object> objs, string logFilePath, string errorFilePath)
        {
            if(objs !=null && objs.Count>0)
            {
                Assembly assem = Assembly.LoadFile(this._CustomTypeAssemblyPath);
                Type type = assem.GetType(this._CustomTypeName);
                object[] classAtts = type.GetCustomAttributes(typeof (OrmClassAttribute), false);
                if(classAtts==null || classAtts.Length !=1)
                {
                    throw new Exception("Wrong type or type is not decorated with [OrmClassAttribute]");
                }
                OrmClassAttribute classAtt = (OrmClassAttribute) classAtts[0];
                string tableName = classAtt.TableName;
                string pkFieldName = classAtt.PkFieldName;
                bool isPkIdentity = classAtt.IsPkIdentityField;
                IDbConnection conn = null;
                if(this._TgtDbProvider==DataProviderType.Oracle)
                {
                    conn = new OracleConnection(this._ConnStr);
                }
                else
                {
                    conn = new SqlConnection(this._ConnStr);
                }
                TableColumn[] cols = DbUtil.GetTableColumns(this._TgtDbProvider, this._ConnStr, string.Empty, tableName);
                if(cols==null || cols.Length==0)
                    throw new Exception("Unable to find table");
                TableColumn keyCol = new TableColumn();
                foreach (TableColumn col in cols)
                {
                    if (col.ColumnName.ToLower() == pkFieldName.ToLower())
                    {
                        keyCol = col;
                        break;
                    }
                }
                if(string.IsNullOrEmpty(keyCol.ColumnName))
                    throw new Exception("Unable to find primary key field in table");
                MemberInfo keyMember = null;
                MemberInfo[] members = type.GetMembers();
                if(members !=null && members.Length>0)
                {
                    foreach(MemberInfo member in members)
                    {
                        string colName = member.Name;
                        object[] memberAtts = member.GetCustomAttributes(typeof (OrmFieldAttribute), false);
                        if(memberAtts !=null && memberAtts.Length==1)
                        {
                            OrmFieldAttribute fieldAtt = (OrmFieldAttribute) memberAtts[0];
                            colName = fieldAtt.ColumnName;
                        }
                        if(colName.ToLower()==keyCol.ColumnName.ToLower())
                        {
                            keyMember = member;
                            break;
                        }
                    }
                }
                if(keyMember==null)
                    throw new Exception("Unable to find corresponding member for PK");

                // init stream writer
                if(File.Exists(logFilePath))
                    File.Delete(logFilePath);
                if(File.Exists(errorFilePath))
                    File.Delete(errorFilePath);
                StreamWriter logWriter=new StreamWriter(logFilePath);
                StreamWriter errWriter=new StreamWriter(errorFilePath);

                try
                {
                    conn.Open();
                    IDbCommand cmd = conn.CreateCommand();
                    for(int i=0;i<objs.Count;i++)
                    {
                        object obj = objs[i];
                        object keyValue = null;
                        if(keyMember.MemberType==MemberTypes.Field)
                        {
                            keyValue = ((FieldInfo) keyMember).GetValue(obj);
                        }
                        else if(keyMember.MemberType==MemberTypes.Property)
                        {
                            keyValue = ((PropertyInfo) keyMember).GetValue(obj, null);
                        }
                        try
                        {
                            bool exist = false;
                            if(keyValue !=null)
                            {
                                string keyValueStr = keyValue.ToString();
                                cmd.CommandText = string.Format("select count (*) from {0} where {1}={2}",
                                                                tableName, keyCol.ColumnName,
                                                                FieldDataType.ToMssqlDataValue(keyCol.DbType,
                                                                                               keyValueStr));
                                
                                try
                                {
                                    int count = int.Parse(cmd.ExecuteScalar().ToString());
                                    if (count > 0)
                                        exist = true;
                                }
                                catch
                                {
                                    exist = false;
                                }
                            }
                            string sql = string.Empty;
                            if (exist)
                            {
                                sql = "update " + tableName + " set ";
                                foreach (TableColumn col in cols)
                                {
                                    if (col.ColumnName.ToLower() != keyCol.ColumnName.ToLower())
                                    {
                                        sql += col.ColumnName + "=" + FieldDataType.ToMssqlDataValue(col.DbType,
                                            this.GetSerializedObjFieldValue(obj, col.ColumnName)) + ",";
                                    }
                                }
                                if (sql.EndsWith(","))
                                {
                                    sql = sql.Substring(0, sql.Length - 1);
                                }
                                sql += " where " + keyCol.ColumnName + "=" + FieldDataType.ToMssqlDataValue(keyCol.DbType,
                                    this.GetSerializedObjFieldValue(obj, keyCol.ColumnName));
                            }
                            else
                            {
                                sql = "insert into " + tableName + "(";
                                foreach (TableColumn col in cols)
                                {
                                    if (!col.IsIdentity)
                                        sql += col.ColumnName + ",";
                                }
                                if (sql.EndsWith(","))
                                {
                                    sql = sql.Substring(0, sql.Length - 1);
                                }
                                sql += ") values (";
                                foreach (TableColumn col in cols)
                                {
                                    if (!col.IsIdentity)
                                    {
                                        sql += FieldDataType.ToMssqlDataValue(col.DbType, this.GetSerializedObjFieldValue(obj, col.ColumnName)) + ",";
                                    }
                                }
                                if (sql.EndsWith(","))
                                {
                                    sql = sql.Substring(0, sql.Length - 1);
                                }
                                sql += ")";
                            }
                            cmd.CommandText = sql;
                            cmd.ExecuteNonQuery();

                            if (isPkIdentity)
                            {
                                // cmd.CommandText = "select max (" + keyFieldName + ") from " + tblName;
                                cmd.CommandText = "SELECT @@IDENTITY";
                                int pkValue = int.Parse(cmd.ExecuteScalar().ToString());
                                {
                                    if (keyMember.MemberType == MemberTypes.Field)
                                    {
                                        ((FieldInfo)keyMember).SetValue(obj, pkValue);
                                    }
                                    else if (keyMember.MemberType == MemberTypes.Property)
                                    {
                                        ((PropertyInfo)keyMember).SetValue(obj, pkValue, null);
                                    }
                                }
                            }

                            objs[i] = obj;
                            if(keyValue==null)
                            {
                                if(keyMember.MemberType==MemberTypes.Field)
                                {
                                    keyValue = ((FieldInfo) keyMember).GetValue(obj);
                                }
                                else if(keyMember.MemberType==MemberTypes.Property)
                                {
                                    keyValue = ((PropertyInfo) keyMember).GetValue(obj, null);
                                }
                            }
                            logWriter.WriteLine("{0}: saved object [{1}]", DateTime.Now.ToString(), keyValue);
                            logWriter.Flush();
                        }
                        catch(Exception ex1)
                        {
                            errWriter.WriteLine("{0}: failed to update object: {1}", DateTime.Now.ToString(),
                                                ex1.ToString());
                            errWriter.Flush();
                        }
                    }
                }
                catch(Exception ex)
                {
                    errWriter.WriteLine(ex.ToString());
                    errWriter.Flush();
                }
                finally
                {
                    conn.Close();
                    logWriter.Close();
                    errWriter.Close();
                }
            }
        }
        #endregion

        #region serialization
        /// <summary>get field value from object</summary>
        public string GetSerializedObjFieldValue(object obj, string colName)
        {
            FieldInfo[] fields = obj.GetType().GetFields();
            if (fields != null && fields.Length > 0)
            {
                foreach (FieldInfo fi in fields)
                {
                    SqlDbType dbType = FieldDataType.ToDbType(fi.FieldType.ToString());
                    OrmFieldAttribute fieldAtt = new OrmFieldAttribute(fi.Name,
                        dbType, FieldDataType.GetFieldDefaultSize(dbType),
                        OrmFieldSerializationMethod.DefaultSerialization);
                    object[] atts = fi.GetCustomAttributes(typeof(OrmFieldAttribute), false);
                    if (atts != null && atts.Length > 0)
                    {
                        foreach (object att in atts)
                        {
                            if (att is OrmFieldAttribute)
                            {
                                fieldAtt = (OrmFieldAttribute)att;
                                break;
                            }
                        }
                    }
                    if (fieldAtt.ColumnName.ToLower() == colName.ToLower())
                    {
                        object val = fi.GetValue(obj);
                        if (val != null)
                        {
                            return this.SerialzieFieldValue(obj.GetType(), fi.Name, val);
                        }
                    }
                }
            }
            PropertyInfo[] props = obj.GetType().GetProperties();
            if (props != null && props.Length > 0)
            {
                foreach (PropertyInfo prop in props)
                {
                    SqlDbType dbType = FieldDataType.ToDbType(prop.PropertyType.ToString());
                    OrmFieldAttribute fieldAtt = new OrmFieldAttribute(prop.Name,
                        dbType, FieldDataType.GetFieldDefaultSize(dbType),
                        OrmFieldSerializationMethod.DefaultSerialization);
                    object[] atts = prop.GetCustomAttributes(typeof(OrmFieldAttribute), false);
                    if (atts != null && atts.Length > 0)
                    {
                        foreach (object att in atts)
                        {
                            if (att is OrmFieldAttribute)
                            {
                                fieldAtt = (OrmFieldAttribute)att;
                                break;
                            }
                        }
                    }
                    if (fieldAtt.ColumnName.ToLower() == colName.ToLower())
                    {
                        object val = prop.GetValue(obj, null);
                        if (val != null)
                        {
                            return this.SerialzieFieldValue(obj.GetType(), prop.Name, val);
                        }
                    }
                }
            }

            return null;
        }

        private string SerialzieFieldValue(Type type, string fieldName, object fieldValue)
        {
            Type fieldType = typeof(string);
            OrmFieldAttribute ormFieldAtt =
                new OrmFieldAttribute(fieldName, SqlDbType.VarChar, 50, OrmFieldSerializationMethod.DefaultSerialization);
            FieldInfo fieldInfo = type.GetField(fieldName);
            if (fieldInfo != null)
            {
                fieldType = fieldInfo.FieldType;
                ormFieldAtt.FieldDataType = FieldDataType.ToDbType(fieldInfo.FieldType.ToString());
                if (fieldInfo.FieldType.IsEnum)
                {
                    fieldValue = (int)fieldValue;
                    ormFieldAtt.FieldDataType = SqlDbType.Int;
                }
                ormFieldAtt.Size = FieldDataType.GetFieldDefaultSize(ormFieldAtt.FieldDataType);

                object[] fieldAtts = fieldInfo.GetCustomAttributes(typeof(OrmFieldAttribute), false);
                if (fieldAtts != null && fieldAtts.Length > 0)
                {
                    foreach (object fieldAtt in fieldAtts)
                    {
                        if (fieldAtt is OrmFieldAttribute)
                        {
                            ormFieldAtt = (OrmFieldAttribute)fieldAtt;
                            break;
                        }
                    }
                }
            }
            else
            {
                PropertyInfo prop = type.GetProperty(fieldName);
                if (prop != null)
                {
                    fieldType = prop.PropertyType;
                    ormFieldAtt.FieldDataType = FieldDataType.ToDbType(prop.PropertyType.ToString());
                    if (prop.PropertyType.IsEnum)
                    {
                        ormFieldAtt.FieldDataType = SqlDbType.Int;
                        fieldValue = (int)fieldValue;
                    }
                    ormFieldAtt.Size = FieldDataType.GetFieldDefaultSize(ormFieldAtt.FieldDataType);
                    object[] propAtts = prop.GetCustomAttributes(typeof(OrmFieldAttribute), false);
                    if (propAtts != null && propAtts.Length > 0)
                    {
                        foreach (object propAtt in propAtts)
                        {
                            if (propAtt is OrmFieldAttribute)
                            {
                                ormFieldAtt = (OrmFieldAttribute)propAtt;
                                break;
                            }
                        }
                    }
                }
                else
                {
                    throw new Exception("Unable to find matching member for field " + fieldName);
                }
            }
            string value = null;
            if (fieldValue != null && fieldValue.GetType() != fieldType && fieldValue.GetType().IsSubclassOf(fieldType))
            {
                fieldType = fieldValue.GetType();
            }

            switch (ormFieldAtt.SerializationMethod)
            {
                case OrmFieldSerializationMethod.DefaultSerialization:
                    value =
                        SimpleDataTypeUtil.ReadValueAsString(
                            SimpleDataTypeUtil.ToSimpleDataType(FieldDataType.ToSystemType(ormFieldAtt.FieldDataType)),
                            fieldValue);
                    break;
                case OrmFieldSerializationMethod.XmlSerialization:
                    XmlSerializer serializer = new XmlSerializer(fieldType);
                    StringBuilder buffer = new StringBuilder();
                    StringWriter sWriter = new StringWriter(buffer);
                    serializer.Serialize(sWriter, fieldValue);
                    sWriter.Flush();
                    value = buffer.ToString();


                    break;
                case OrmFieldSerializationMethod.BinarySerialization:
                    BinaryFormatter bSerializer = new BinaryFormatter();
                    string tmpFilePath = FileUtil.GetWindowsAppTempFilePath("ORMapping", ".orm");
                    FileStream fs = File.OpenWrite(tmpFilePath);
                    bSerializer.Serialize(fs, fieldValue);
                    fs.Close();

                    fs = File.OpenRead(tmpFilePath);
                    byte[] contents = new byte[fs.Length];
                    fs.Read(contents, 0, contents.Length);
                    fs.Close();
                    value = Base64Util.EncodeAsBase64(contents);

                    File.Delete(tmpFilePath);


                    break;
                case OrmFieldSerializationMethod.CustomSerializationMethod:
                    throw new NotImplementedException();
                default:
                    break;
            }
            return value;
        }
        #endregion
    }
}
